Skip to content

iOS WKWebView 使用 WKURLSchemeHandler 拦截图片请求并实现缓存

随着 WKWebView 在 iOS 中逐渐取代 UIWebView,越来越多的 App 在业务场景中需要对 Web 内容进行深度定制。一个典型需求是 对图片等静态资源的请求进行拦截和缓存,以提升加载速度和用户体验。

从 iOS 11 开始,Apple 提供了 WKURLSchemeHandler 协议,开发者可以通过自定义 Scheme 来接管资源请求逻辑。本文将详细介绍如何使用 Objective-C 实现图片请求拦截,并结合客户端缓存策略,构建一个更可控的资源加载机制。

1. 为什么需要拦截图片请求?

常见的业务痛点包括:

  • 重复下载:网页中相同图片可能被多次请求,浪费流量。
  • 弱网/离线优化:需要支持预加载和离线缓存。
  • 统一鉴权:某些资源需要额外的 Token 或签名验证。
  • 内容替换:在某些场景下需要屏蔽或替换部分图片。

使用 WKURLSchemeHandler,我们可以实现一个类似「自研 CDN」的机制,把资源交由客户端掌控。

2. WKURLSchemeHandler 基本原理

  • 注册自定义 Scheme:在 WKWebViewConfiguration 中绑定 Handler。
  • 拦截请求:当网页发起 customimg:// 形式的请求时,会进入 WKURLSchemeHandler 回调。
  • 返回响应:Handler 需要主动构造 NSURLResponseNSData,再回调给 WKWebView

⚠️ 注意:WKURLSchemeHandler 只能拦截自定义 Scheme,无法直接拦截 http/https。因此我们需要在 HTML 中对图片链接做替换。

3. 实现步骤(Objective-C)

3.1 注册 Handler

objective-c
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
ImageSchemeHandler *handler = [[ImageSchemeHandler alloc] init];

// 注册 customimg 协议
[configuration setURLSchemeHandler:handler forURLScheme:@"customimg"];

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds
                                        configuration:configuration];
[self.view addSubview:webView];

在加载 HTML 时,需将 <img src="https://..."> 替换为 <img src="customimg://...">,才能被拦截。

3.2 自定义 Handler

objective-c
@interface ImageSchemeHandler : NSObject <WKURLSchemeHandler>
@property (nonatomic, strong) NSCache *memoryCache;
@end

@implementation ImageSchemeHandler

- (instancetype)init {
    if (self = [super init]) {
        _memoryCache = [[NSCache alloc] init];
    }
    return self;
}

#pragma mark - WKURLSchemeHandler

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    NSURL *url = urlSchemeTask.request.URL;
    if (!url) return;

    // 1. 先查内存缓存
    NSData *cachedData = [self.memoryCache objectForKey:url.absoluteString];
    if (cachedData) {
        [self respondWithData:cachedData forTask:urlSchemeTask];
        return;
    }

    // 2. 查磁盘缓存
    NSString *cachePath = [self cachePathForURL:url];
    if ([[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
        NSData *diskData = [NSData dataWithContentsOfFile:cachePath];
        if (diskData) {
            [self.memoryCache setObject:diskData forKey:url.absoluteString];
            [self respondWithData:diskData forTask:urlSchemeTask];
            return;
        }
    }

    // 3. 无缓存 -> 下载
    NSURL *originURL = [self convertCustomURLToOrigin:url];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:originURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error || !data) {
            [urlSchemeTask didFailWithError:error ?: [NSError errorWithDomain:@"ImageDownload" code:-1 userInfo:nil]];
            return;
        }

        // 写入缓存
        [self.memoryCache setObject:data forKey:url.absoluteString];
        [data writeToFile:cachePath atomically:YES];

        // 返回数据
        [self respondWithData:data forTask:urlSchemeTask];
    }];
    [task resume];
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    // 可在此中断请求,例如取消 NSURLSessionTask
}

#pragma mark - Helper

- (void)respondWithData:(NSData *)data forTask:(id<WKURLSchemeTask>)task {
    NSString *mimeType = @"image/png"; // 可根据文件后缀判断
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:task.request.URL
                                                       MIMEType:mimeType
                                          expectedContentLength:data.length
                                               textEncodingName:nil];
    [task didReceiveResponse:response];
    [task didReceiveData:data];
    [task didFinish];
}

- (NSString *)cachePathForURL:(NSURL *)url {
    NSString *fileName = [url.absoluteString stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
    NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    return [cacheDir stringByAppendingPathComponent:fileName];
}

- (NSURL *)convertCustomURLToOrigin:(NSURL *)customURL {
    // 示例:customimg://example.com/img.png -> https://example.com/img.png
    NSString *origin = [customURL.absoluteString stringByReplacingOccurrencesOfString:@"customimg://" withString:@"https://"];
    return [NSURL URLWithString:origin];
}

@end

4. 磁盘缓存策略优化

  1. 过期时间 可以在写入缓存时,额外保存一个 metadata 文件,记录下载时间和过期时间。

  2. 缓存清理

    • 定期扫描缓存目录,清理过期或过大的文件;
    • 设置缓存大小上限(如 100MB)。
  3. MIME 类型识别 可以通过响应头 Content-Type 来动态确定 mimeType,而不是写死为 image/png

5. 实际应用场景

  • 离线模式:提前缓存常用图片,弱网环境下也能快速加载。
  • 鉴权下载:拦截请求后,可以在下载逻辑中加上 Token 或自定义 Header。
  • 动态替换:某些图片可以在拦截层直接替换为本地资源,实现敏感内容屏蔽。

6. 注意事项

  • 只能拦截 自定义 Scheme,需要在 HTML 中替换图片链接。
  • 不要在主线程进行下载或磁盘 IO,避免卡顿。
  • 注意内存缓存的释放,避免大图占用过多内存。

7. 总结

通过 WKURLSchemeHandler,我们可以为 WKWebView 构建一个灵活的资源加载管道。在 Objective-C 中实现时,可以结合 内存缓存 + 磁盘缓存,并复用客户端已有的下载逻辑,实现稳定且高性能的图片加载。

这一方案不仅能有效减少重复下载,还能更好地适配弱网、离线场景,甚至实现资源级别的安全控制。对于需要深度定制 WebView 行为的应用,这是一个非常实用的技术手段。

Released under the MIT License.